#!/bin/sh
############################################################################
# 
# Tool for generation RSS feeds for web sites that does not have RSS feed
#
# Copyright (c) 2013-14, Alexander Galanin <al@galanin.nnov.ru>
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
# * Redistributions of source code must retain the above copyright notice,
#   this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in the
#   documentation and/or other materials provided with the distribution.
# * Neither the name of Alexander Galanin nor the names of its contributors
#   may be used to endorse or promote products derived from this software
#   without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL Alexander Galanin BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
############################################################################
# \
exec tclsh8.5 "$0" "$@"

package require Tcl 8.5
package require http
package require cmdline
package require logger 0.9.3

variable version 0.2.0
variable app "RSSFromAnywhere $version"

set dir [file dirname [info script]]
lappend auto_path [file join $dir lib] [file join $dir httplib-tcl]

package require mediator
package require rss
package require cache

############################################################################
# FUNCTIONS
############################################################################

namespace eval main {

::logger::initNamespace [namespace current]

namespace export feed

variable outDir .
variable failed false

# Make feed with specified options
# @param dest destination file
# @param url web page URL
# @param options feed options
# @return number of items in RSS feed
proc feed {dest url options} {
    variable failed

    httplib clearReferer

    set count 0
    if {[catch {
        set count [runFeed $url data $options]
        # warn if there are no items in the feed
        if {$count == 0} {
            log::warn "$url: $title: fetched zero items"
        }
        saveFeed $dest $data
    } err opts]} {
        set failed true
        # log non-fatal errors to stderr
        set code [lindex [dict get $opts -errorcode] 0]
        if {$code in {DOWNLOADER MATCH}} {
            log::error "$dest: $err"
        } elseif {$code eq "USERSCRIPT"} {
            log::error "$dest: error in user script: [dict get $opts -oldoptions -errorinfo]"
        } else {
            return -options $opts $err
        }
    }
    return $count
}

# Assign fedd parameters to variables in the caller context
proc assignFeedParameters {options} {
    upvar additionalParams params

    foreach key {title description list item command} {
        uplevel [list set $key [dict get $options $key]]
    }
    set params {}
    if {[dict exists $options additional]} {
        dict set params -info [list [dict get $options additional]]
    }
    if {[dict exists $options maxage]} {
        dict set params -mintime [clock scan -[dict get $options maxage]]
    } else {
        dict set params -mintime 0
    }
    if {[dict exists $options matcher]} {
        dict set params -matcher [list [dict get $options matcher]]
    }
}

# Run feed generation
# @param url page url
# @param dataVar variable name to store data
# @param options feed options
# @return number of items in the feed
proc runFeed {url dataVar options} {
    upvar $dataVar data

    assignFeedParameters $options
    set rss [rss begin $url $title $description]
    set count [mediator run $url \
        $list \
        $item \
        $command \
        [list rss addItem $rss] \
        [rss requiredFields] \
        {*}$additionalParams \
    ]
    set data [rss end $rss]
    return $count
}

# Save generated feed into file in output directory
# @param dest destination file name
# @param data feed content
proc saveFeed {dest data} {
    variable outDir
    variable failed

    set fname [file join $outDir $dest]
    if {[catch {
        # create temporary file
        set f [open $fname.new w]
        fconfigure $f -encoding utf-8
        puts $f $data
        close $f
        # overwrite file
        file rename -force $fname.new $fname
    } err opts]} {
        catch {close $f}
        puts stderr "$fname: $err"
        set failed true
    }
}

}

############################################################################
# MAIN
############################################################################

namespace import ::main::feed

set week [expr {60 * 60 * 24 *7}]
set cacheDb [file join $env(HOME) .rss-from-anywhere-cache.sqlite3]
set options [list \
    [list cache.arg  $cacheDb   "Cache database location"] \
    {out.arg        "."     "Output directory for generated files"} \
    {cleanup                "Clean up old cache items"} \
    {loglevel.arg   warn    "Log level (debug, info, notice, warn, error, critical)"} \
    [list cachetimeout.arg   $week   "Timeout for cache items for -cleanup (sec)"] \
]
set usage {[options] <feedfile>}
if {[catch {array set params [::cmdline::getoptions argv $options $usage]} err]} {
    puts stderr $err
    exit 1
}
if {[llength $argv] != 1} {
    puts stderr [cmdline::usage $options $usage]
    exit 1
}
lassign $argv feedfile

::logger::setlevel $params(loglevel)
set ::main::outDir $params(out)
rss init $app
httplib init \
    -useragent $app
cache init $params(cache)

if {$params(cleanup)} {
    cache cleanup $params(cachetimeout)
} elseif {$feedfile eq "-"} {
    eval [read stdin]
} else {
    source -encoding utf-8 $feedfile
}

cache destroy

if {$::main::failed} {
    exit 1
}
